commonlibsse_ng\rel\module/mod.rs
1// C++ Original code
2// - https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/REL/Module.h
3// - load_segments, clear: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/src/REL/Module.cpp
4// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
5// SPDX-License-Identifier: MIT
6//
7// SPDX-FileCopyrightText: (C) 2025 SARDONYX
8// SPDX-License-Identifier: Apache-2.0 OR MIT
9
10//! Module handling library for Skyrim SE/AE/VR .
11
12mod module_core;
13mod module_handle;
14mod runtime;
15mod segment;
16
17pub use self::module_core::{Module, ModuleInitError};
18pub use self::module_handle::{ModuleHandle, ModuleHandleError};
19pub use self::runtime::{Runtime, get_skyrim_dir, get_skyrim_exe_path};
20pub use self::segment::{Segment, SegmentName};
21
22use std::sync::{LazyLock, RwLock};
23
24static MODULE: LazyLock<RwLock<ModuleState>> = LazyLock::new(|| RwLock::new(ModuleState::new()));
25
26/// Returns `true` if the current execution environment is `SkyrimVR.exe`.
27///
28/// ### When to use
29/// - Use this to simply distinguish VR from SE/AE, e.g., for branching based on class layout.
30///
31/// ### When *not* to use
32/// - If you also need other runtime info from [`ModuleState`].
33/// Calling this separately can cause redundant locking; prefer accessing [`ModuleState`] directly instead.
34///
35/// ### Note
36/// Internally uses [`ModuleState::map_or_init`]; returns `false` on init failure or if not VR.
37#[inline]
38pub fn is_vr() -> bool {
39 ModuleState::map_or_init(|m| m.runtime.is_vr()).ok().is_some_and(|is_vr| is_vr)
40}
41
42/// Represents the state of the module.
43///
44/// This enum implements an API to manage a single global variable of internally managed module (e.g. `SkyrimSE.exe`) information.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum ModuleState {
47 /// The module is successfully initialized and active.
48 Active(Module),
49
50 /// The module instance has been explicitly cleared and memory has been freed.
51 Cleared,
52
53 /// The module failed to initialize.
54 FailedInit(ModuleInitError),
55}
56
57impl ModuleState {
58 /// Initialize the module.
59 fn new() -> Self {
60 let module = Module::new();
61 #[cfg(feature = "test_on_ci")]
62 let module = module.or_else(|_| Module::new_with_msvcrt());
63 #[cfg(feature = "test_on_local")]
64 let module = module.or_else(|_| Module::new_from_skyrim_exe());
65 match module {
66 Ok(module) => Self::Active(module),
67 Err(err) => Self::FailedInit(err),
68 }
69 }
70
71 /// Attempts to apply a function to the active module state.
72 ///
73 /// This function tries to acquire a read lock on the module state and applies
74 /// the provided function `f` if the module state is [`ModuleState::Active`].
75 ///
76 /// If you do not know when you did the [`Self::reset`], or if you want it to be reinitialized automatically if necessary,
77 /// [`Self::map_or_init`] is useful.
78 ///
79 /// # Example
80 /// ```
81 /// use commonlibsse_ng::rel::module::ModuleState;
82 ///
83 /// let result = ModuleState::map_active(|module| module.version.clone());
84 /// match result {
85 /// Ok(version) => println!("Module version: {}", version),
86 /// Err(err) => eprintln!("Error: {:?}", err),
87 /// }
88 /// ```
89 ///
90 /// # Errors
91 /// Returns an error if:
92 /// - The module state is [`ModuleState::Cleared`].
93 /// - The module state is [`ModuleState::FailedInit`], in which case the initialization error is propagated.
94 /// - The internal lock is poisoned.
95 #[inline]
96 pub fn map_active<F, T>(f: F) -> Result<T, ModuleStateError>
97 where
98 F: FnOnce(&Module) -> T,
99 {
100 let guard = MODULE.read().map_err(|_| ModuleStateError::ModuleLockIsPoisoned)?;
101
102 match &*guard {
103 Self::Active(module) => Ok(f(module)),
104 Self::Cleared => Err(ModuleStateError::ModuleHasBeenCleared),
105 Self::FailedInit(module_init_error) => {
106 Err(ModuleStateError::FailedInit { source: module_init_error.clone() })
107 }
108 }
109 }
110
111 /// Attempts to apply a function to the active module state, initializing it if necessary.
112 ///
113 /// If the module state is `Cleared`/`FailedInit`, it will be initialized before applying the function `f`.
114 /// This function also attempts a read lock first and falls back to initialization only if needed.
115 ///
116 /// # Example
117 /// ```
118 /// use commonlibsse_ng::rel::module::ModuleState;
119 ///
120 /// let result = ModuleState::map_or_init(|module| module.version.clone());
121 /// match result {
122 /// Ok(version) => println!("Module version: {}", version),
123 /// Err(err) => eprintln!("Error: {:?}", err),
124 /// }
125 /// ```
126 ///
127 /// # Errors
128 /// Returns an error if:
129 /// - The module state is [`ModuleState::FailedInit`], in which case the initialization error is propagated.
130 /// - The internal lock is poisoned.
131 pub fn map_or_init<F, T>(f: F) -> Result<T, ModuleStateError>
132 where
133 F: FnOnce(&Module) -> T,
134 {
135 if let Ok(guard) = MODULE.read() {
136 if let Self::Active(module) = &*guard {
137 return Ok(f(module));
138 }
139 }
140
141 // The fact that it was not `Active` means that it absolutely needs to be initialized.
142 let (ret, module_state) = match Module::new() {
143 Ok(module) => (Ok(f(&module)), Self::Active(module)),
144 Err(err) => {
145 let ret_err = ModuleStateError::FailedInit { source: err.clone() };
146 (Err(ret_err), Self::FailedInit(err))
147 }
148 };
149
150 // Delaying lock acquisition to avoid prolonged lock acquisition.
151 MODULE
152 .write()
153 .map(|mut guard| *guard = module_state)
154 .map_err(|_| ModuleStateError::ModuleLockIsPoisoned)?;
155
156 ret
157 }
158
159 /// Clears the module, transitioning it to the `Cleared` state.
160 ///
161 /// # Example
162 /// ```
163 /// use commonlibsse_ng::rel::module::ModuleState;
164 ///
165 /// assert!(ModuleState::reset().is_ok());
166 /// ```
167 ///
168 /// # Errors
169 /// If the thread that had previously acquired a lock on the singleton instance panics, an error is returned.
170 #[inline]
171 pub fn reset() -> Result<(), ModuleStateError> {
172 MODULE.write().map_or(Err(ModuleStateError::ModuleLockIsPoisoned), |mut guard| {
173 *guard = Self::Cleared;
174 Ok(())
175 })
176 }
177}
178
179/// Type definition for treating an instance of information management as an error when it is in
180/// a state where information cannot be obtained.
181#[derive(Debug, Clone, snafu::Snafu)]
182pub enum ModuleStateError {
183 /// The thread that was getting Module's lock panicked.
184 ModuleLockIsPoisoned,
185
186 /// Module has been cleared
187 ModuleHasBeenCleared,
188
189 /// Module initialization error
190 #[snafu(display("Module initialization error: {source}"))]
191 FailedInit { source: crate::rel::module::ModuleInitError },
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_module_reset() {
200 assert!(ModuleState::reset().is_ok());
201 }
202}